#!/usr/local/bin/python3
# *_* coding: UTF-8 *_*
# @IDE: PyCharm
# @Version: Python3.7
# @Author: Kendrick.Kc
# @Email: 509556985@qq.com
# @File: __init__.py.py
# @Inst: 回测模块
# @Time: 2023/7/15 21:37
# -----
# import package
# data = package.VBT().get_data('1h')


import json
import time
import os
import inspect
import sys
import pandas
import talib
import numpy
import pandas_ta
import vectorbt
import sqlite3
import datetime
import threading
import requests
from multiprocessing import Process
pandas.set_option('expand_frame_repr', False)  # 当列太多时不换行
pandas.set_option('display.max_rows', 10000)  # 最多显示数据的行数

from ROOT.settings import BASE_DIR

"""
DB现有交易对：['XRPUSDT', 'BTCUSDT', 'EOSUSDT', 'BNBUSDT', 'ETHUSDT', 'ADAUSDT', 'DOGEUSDT', 'LTCUSDT', 'FILUSDT']
DB现有时间周期：['1h', '15m', '1w', '3m', '6h', '4h', '12h', '30m', '1m', '2h', '5m', '3d', '8h', '1d']
"""
db_path = BASE_DIR / f"apps/backtest/binance_futures_data.db"  # 回测的数据库文件名(百度网盘有)
fear_and_greed_json_path = BASE_DIR / f"apps/backtest/fear_and_greed_index.json"


class SaveExchangeBacktestData:
    """
    续存交易所回测数据
    实例化类就可直接使用
    """
    symbol_list = list()  # 现有交易对列表
    time_cycle_list = list()  # 现有时间周期列表
    ready_data_list = list()  # 获取数据前的准备参数
    con = sqlite3.connect(db_path)  # 连接数据库
    cursor = con.cursor()

    def __init__(self):
        self.get_symbol_time()
        self.get_limit_data()
        self.get_exchange_data()
        self.close_db()

        # for item in self.ready_data_list:
        #     self.logs(item)

    def get_symbol_time(self):
        """ 获取现有数据表名并分离出交易对名称及时间周期 """
        sql = "SELECT name FROM sqlite_master WHERE type='table';"
        self.cursor.execute(sql)
        tables = self.cursor.fetchall()
        for table in tables:
            str_list = table[0].split('-')
            self.symbol_list.append(str_list[0])
            self.time_cycle_list.append(str_list[1])
        self.symbol_list = list(set(self.symbol_list))
        self.time_cycle_list = list(set(self.time_cycle_list))

    def get_limit_data(self):
        """ 获取数据库最后一行数据的date作为起始时间戳 """
        for symbol in self.symbol_list:
            for cycle in self.time_cycle_list:
                symbol_time = f"{symbol}-{cycle}"
                sql = f"SELECT date FROM `{symbol_time}` ORDER BY date DESC LIMIT 1;"
                self.cursor.execute(sql)
                tables = self.cursor.fetchall()
                self.ready_data_list.append({
                    "symbol_time": symbol_time,
                    "symbol": symbol,
                    "time": cycle,
                    "start_time": str(tables[0][0])
                })

    def get_exchange_data(self, url="https://fapi.binance.com/fapi/v1/continuousKlines", limit=1000):
        """ 从交易所循环获取数据 """
        for ready in self.ready_data_list:
            start_time = ready["start_time"]
            while True:
                params = {
                    "pair": ready["symbol"],
                    "contractType": "PERPETUAL",
                    "interval": ready["time"],
                    "startTime": start_time,
                    "limit": limit
                }
                res_data = requests.get(url=url, params=params).json()[1:]

                if not res_data:
                    print(f'{ready["symbol_time"]}数据获取结束.')
                    break

                res_data = pandas.DataFrame(res_data)
                data = res_data[[0, 1, 2, 3, 4, 5]]
                data.columns = ["date", "open", "high", "low", "close", "vol"]
                data.to_sql(name=ready["symbol_time"], con=self.con, if_exists='append', index=False)
                start_time = data["date"].values[-1]

                time.sleep(1)

    def close_db(self):
        self.cursor.close()
        self.con.close()


class VBT:
    """
    用法
        class MyStrategy(backtest.VBT):
            # 其它初始参数跳转到类

            def clean_data(self):
                # 构建指标函数，在回测前使用

            def strategy(self, data):
                # 数据循环函数，按行循环
                # data为当前行数据

            def finish(self):
                # 统计数据完成后的预留函数

        run = MyStrategy()
        run.run_strategy(run.strategy)
    """
    start_time = None  # 回测数据的开始日期
    end_time = None  # 回测数据的结束日期
    initial_capital = 1000  # 初始资金
    commission = 0.0004  # 手续费
    slip_point = 0.001     # 百分比表示滑点    -   0.001%
    symbol = "BTCUSDT"  # 交易对
    i_data = None   # 当前KBar的下标
    n_data = None   # 下一个KBar的下标
    pf = None   # 回测结果
    pf_show = True     # 是否展示交易图表(默认展示)
    buy_long = "BuyLong"       # 买入开多
    sell_short = "SellShort"   # 卖出开空
    sell_long = "SellLong"     # 卖出平多
    buy_short = "BuyShort"     # 买入平空
    long_pos = False            # 多仓是否持仓    -   True/持仓, False/不持仓
    short_pos = False           # 空仓是否持仓    -   True/持仓, False/不持仓
    long_pos_win_price = None         # 持仓止盈价
    long_pos_exits_price = None       # 持仓止损价
    long_pos_yi = None          # 持仓移动止损百分比
    short_pos_win_price = None        # 持仓止盈价
    short_pos_exits_price = None      # 持仓止损价
    short_pos_yi = None         # 持仓移动止损百分比
    vbt_long_entries = []       # 开多仓
    vbt_long_exits = []         # 平多仓
    vbt_short_entries = []      # 开空仓
    vbt_short_exits = []        # 平空仓

    def __init__(self):
        self.data = self.get_data()
        self.data = self.fear_and_greed_index()
        self.initializes_the_number_shapes()

    def initializes_the_number_shapes(self):
        """ 初始化形状数量，否则无法执行完整回撤 """
        self.vbt_long_entries = pandas.Series([False] * len(self.data))
        self.vbt_long_exits = pandas.Series([False] * len(self.data))
        self.vbt_short_entries = pandas.Series([False] * len(self.data))
        self.vbt_short_exits = pandas.Series([False] * len(self.data))

    def fear_and_greed_index(self):
        """
        现有数据合并恐慌与情绪指数
        :return:
        """
        with open(fear_and_greed_json_path, 'r') as f:
            data = json.load(f)
        data = [{"fgi": item["value"], "timestamp": item["timestamp"]} for item in data["data"]]
        data = pandas.DataFrame(data)
        data['fgi'] = data['fgi'].astype('float')
        data['timestamp'] = pandas.to_datetime(data['timestamp']) + datetime.timedelta(hours=8)
        data.sort_values('timestamp', inplace=True)
        data.set_index('timestamp', inplace=True)
        data = pandas.concat([self.data, data], axis=1, join='outer')
        data["fgi"].fillna(method='ffill', inplace=True)    #
        data = data.dropna()
        return data

    def get_data(self, bar: str = "1m"):
        """
        获取回测数据
        :return: dataframe
        """
        con = sqlite3.connect(db_path)  # 连接数据库
        sql = f"select * from `{self.symbol}{'-'}{bar}`"
        if self.start_time is not None:
            st = str(int(datetime.datetime.strptime(self.start_time, "%Y-%m-%d").timestamp() * 1000))
            et = str(int(datetime.datetime.strptime(self.end_time, "%Y-%m-%d").timestamp() * 1000))
            sql = f"select * from `{self.symbol}{'-'}{bar}` where date>='{st}' and date<='{et}'"
        data = pandas.read_sql(sql=sql, con=con)
        data = data.astype('float')
        data['date_index'] = pandas.to_datetime(data['date'], unit='ms') + datetime.timedelta(hours=8)
        data['date'] = data['date_index']
        data.set_index('date_index', inplace=True)
        return data

    def run_strategy(self, strategy):
        """
        运行策略
        :param strategy: 调用策略方法
        :return:
        """
        self.logs()
        self.logs(f"#######################################################")
        self.logs("## 回测开始......")
        self.logs(f"#######################################################")
        self.logs()

        self.clean_data()
        line_number = len(self.data)
        for i in range(line_number):
            if (i + 1) == line_number:
                break

            self.i_data = i
            strategy(self.data.iloc[i])

        self.series_from_signals()

    def entries_pos(self, side=None):
        """
        开仓 - 开仓时是用的下一根Kbar
        :param side: 方向
        :return:
        """
        msg = f"{self.data.iloc[self.i_data].fgi} | {self.data.iloc[self.i_data].date} | "
        if side == self.buy_long:
            self.long_pos = True
            self.vbt_long_entries[self.i_data] = True
            self.logs(f"{msg}[{self.buy_long}]Price:{self.data.iloc[self.i_data].close}")

        if side == self.sell_short:
            self.short_pos = True
            self.vbt_short_entries[self.i_data] = True
            self.logs(f"{msg}[{self.sell_short}]Price:{self.data.iloc[self.i_data].close}")

    def exits_pos(self, side=None):
        """
        平仓 - 开仓时是用的下一根Kbar
        :param side: 方向
        :return:
        """
        msg = f"{self.data.iloc[self.i_data].fgi} | {self.data.iloc[self.i_data].date} | "
        if side == self.sell_long:
            self.long_pos = False
            self.vbt_long_exits[self.i_data] = True
            self.logs(f"{msg}[{self.sell_long}]Price:{self.data.iloc[self.i_data].close}")

        if side == self.buy_short:
            self.short_pos = False
            self.vbt_short_exits[self.i_data] = True
            self.logs(f"{msg}[{self.buy_short}]Price:{self.data.iloc[self.i_data].close}")

    def series_from_signals(self):
        """
        构建交易数据
        :return:
        """

        self.logs()
        self.logs(f"#######################################################")
        self.logs("## 统计数据")
        self.logs(f"#######################################################")
        self.logs()

        self.pf = vectorbt.Portfolio.from_signals(close=self.data['close'],
                                                  entries=self.vbt_long_entries,            # 开多仓
                                                  exits=self.vbt_long_exits,                # 平多仓
                                                  short_entries=self.vbt_short_entries,     # 开空仓
                                                  short_exits=self.vbt_short_exits,         # 平空仓
                                                  init_cash=self.initial_capital,           # 初始资金
                                                  fees=self.commission,                     # 手续费
                                                  slippage=self.slip_point,                 # 滑点
                                                  )
        self.pf_data_show()

    @staticmethod
    def logs(messages=""):
        """ 日志 """
        print(f"{messages}")

    def pf_data_show(self):
        """
        回测结果
        :return:
        """
        # 回测结果
        self.logs(self.pf.stats())
        self.finish()

        # 交易详情
        # self.logs("\n", self.pf.orders.records_readable)

        self.logs()
        self.logs(f"#######################################################")
        self.logs(f"## 正在构建图表并预览到浏览器中，可能需要点时间，请耐心等待......")
        self.logs(f"#######################################################")

        if self.pf_show:
            # 展示交易图表 - width长度
            # pf.orders.plot(width=1000).show()

            # 图表
            self.pf.plot().show()

    def current_path_splice(self, file_name: str = ""):
        """ 与当前目录路径拼接 """
        current_file_path = os.path.abspath(inspect.stack()[1].filename)
        current_dir = os.path.dirname(current_file_path)
        file_path = os.path.join(current_dir, file_name)
        self.logs(f"\n回测结果文件路径: {file_path}")
        return file_path

    def clean_data(self):
        """
        清洗数据
        :return:
        """
        pass

    def finish(self):
        """
        结束统计数据后的结束函数
        """
        pass
